/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2023年4月7日
 * V4.0
 */
package com.jphenix.share.tools;

import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.util.Random;
import javax.imageio.ImageIO;
import com.jphenix.standard.docs.ClassInfo;
import com.jphenix.standard.servlet.api.IRequest;
import com.jphenix.standard.servlet.api.IResponse;

/**
 * 滑动图块验证类
 * 
 * com.jphenix.share.tools.SlipImage
 * 
 * 原理：
 * 
 * 	1. 程序随机选择一个背景图片
 *  2. 将阴影图片已特定的透明度与背景图片结合
 *  3. 阴影图片合成到背景图片的横坐标为随机值
 *  4. 前台获取到背景图片和滑块图片
 *  5. 通过鼠标拖拽滑块图片到指定阴影位置
 *  6. 提交滑块图片横坐标像素位置值到后台
 *  7. 程序比较提交值与随机阴影横坐标位置值是否匹配
 *  
 *  建议模式1：拖动滑块到指定位置松手提交登录信息和滑块横坐标位置信息执行登录
 *  特点：减少页面操作步骤
 *  
 *  建议模式2：
 *    (1) 录入登录名，密码，拖动滑块位置松手后，单独提交滑块位置值
 *    (2) 程序在后台验证滑块位置符合要求后，随机生成一个key值，保存到会话中，也同时输出到页面端
 *    (3) 在页面中点击登录，将这个key值连同登录信息提交后台
 *    (4) 程序在后台将提交的key值与会话中的key值进行比对是否一致，比对后从会话中删除key值。
 *  特点：
 *    (1) 滑动图块程序相对独立，适合改造之前的录入图片信息式的验证方式，几乎不改动原有验证代码
 *    (2) 避免验证码错误导致频繁提交登录信息
 * 
 * 注意：
 *    
 *    1. 要求背景图片，阴影图片，滑块图片均为PNG格式
 *    2. 设置背景图片数后，程序会在根文件夹按照背景图片前缀检索文件，比如： bg_1.png bg_2.png
 *    3. 背景图片序号从1开始，包含bgImageFileCount的值
 *    3. 滑块随机位置在背景图片横向 30%位置开始，90%位置处结束
 *    
 * 使用方法：
 * 
 * 	 通常将该类放到常驻内存的服务中，在这个服务中写个方法返回SlipImage类实例到动作类中，在动作类中调用
 * 
 * @author MBG
 * 2023年4月7日
 */
@ClassInfo({"2023-04-07 20:15","滑动图块验证类"})
public class SlipImage {

	//#region 【变量声明区】
	// 图片根路径
	private String imageBasePath = null;
	
	// 背景图片文件名前缀
	private String bgImageFileNameHead = "bg_";
	
	// 阴影图片文件名
	private String maskImageName = "shadow.png";
	
	// 划款图片文件名
	private String blockImageName = "block.png";
	
	// 页面请求提交图片横向位置值的参数主键
	private String valueRequestKey = "image_value";
	
	// 阴影横向位置值，保存在会话中的主键名
	private String valueSessionKey = "session_slip_image_value";
	
	// 在验证成功后，需要往会话中设置随机数值的主键（建议模式2）
	private String otherCheckSessionKey = "";
	
	// 背景图片总数
	private int    bgImageFileCount = 0;
	
	// 阴影图片在背景图片中的纵向位置，默认负数时，为背景图片的四分之一位置 （从上往下 像素）
	private int    maskImageTop = -1;
	
	// 输出信息缓存大小
	private int    bufferSize = 1024;
	
	// 用户操作允许误差（左右单位误差值 不是左右误差值和的值）
	private int    errorsValue = 5;
	
	// 序列号索引值
	private long   index = 0;
	
	// 阴影图块透明度
	private float  opacity = 0.4f;
	
	// 随机数对象
	private Random random = new Random();
	
	// 阴影块图片缓存对象
	private BufferedImage maskBi = null;
	//#endregion
	
	//#region 【构造函数】
	/**
	 * 构造函数
	 * @author MBG
	 */
	public SlipImage() {
		super();
	}
	//#endregion
	
	//#region 【Getter Setter 区】
	
	//#region opacity
	/**
	 * 设置阴影图块透明度
	 * @param val 阴影图块透明度
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setOpacity(float val) {
		if(val>1 || val<0) {
			return;
		}
		this.opacity = val;
	}
	
	/**
	 * 获取阴影图块透明度
	 * @return 阴影图块透明度
	 * 2023年4月7日
	 * @author MBG
	 */
	public float getOpacity() {
		return opacity;
	}
	//#endregion
	
	//#region otherCheckSessionKey
	/**
	 * 设置在验证成功后，需要往会话中设置随机数值的主键（建议模式2）
	 * @param key 在验证成功后，需要往会话中设置随机数值的主键
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setOtherCheckSessionKey(String key) {
		if(key==null || key.length()<1) {
			return;
		}
		this.otherCheckSessionKey = key;
	}
	
	/**
	 * 获取在验证成功后，需要往会话中设置随机数值的主键（建议模式2）
	 * @return 在验证成功后，需要往会话中设置随机数值的主键
	 * 2023年4月7日
	 * @author MBG
	 */
	public String getOtherCheckSessionKey() {
		return otherCheckSessionKey;
	}
	//#endregion
	
	//#region valueRequestKey
	/**
	 * 设置页面请求提交图片横向位置值的参数主键
	 * @param key 页面请求提交图片横向位置值的参数主键
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setValueRequestKey(String key) {
		this.valueRequestKey = key;
	}
	
	/**
	 * 获取页面请求提交图片横向位置值的参数主键
	 * @return 页面请求提交图片横向位置值的参数主键
	 * 2023年4月7日
	 * @author MBG
	 */
	public String getValueRequestKey() {
		return valueRequestKey;
	}
	//#endregion
	
	//#region errorsValue
	/**
	 * 设置用户操作允许误差（左右单位误差值 不是左右误差值和的值）
	 * @param value 用户操作允许误差
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setErrorsValue(int value) {
		if(value<1) {
			return;
		}
		this.errorsValue = value;
	}
	
	/**
	 * 获取用户操作允许误差（左右单位误差值 不是左右误差值和的值）
	 * @return 用户操作允许误差
	 * 2023年4月7日
	 * @author MBG
	 */
	public int getErrorsValue() {
		return errorsValue;
	}
	//#endregion
	
	//#region valueSessionKey
	/**
	 * 设置阴影横向位置值，保存在会话中的主键名
	 * @param key 阴影横向位置值，保存在会话中的主键名
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setValueSessionKey(String key) {
		if(key==null || key.length()<1) {
			return;
		}
		this.valueSessionKey = key;
	}
	
	/**
	 * 获取阴影横向位置值，保存在会话中的主键名
	 * @return 阴影横向位置值，保存在会话中的主键名
	 * 2023年4月7日
	 * @author MBG
	 */
	public String getValueSessionKey() {
		return valueSessionKey;
	}
	//#endregion
	
	//#region bufferSize
	/**
	 * 设置输出信息缓存大小
	 * @param size 输出信息缓存大小
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setBufferSize(int size) {
		if(size<1) {
			return;
		}
		this.bufferSize = size;
	}
	
	/**
	 * 获取输出信息缓存大小
	 * @return 输出信息缓存大小
	 * 2023年4月7日
	 * @author MBG
	 */
	public int getBufferSize() {
		return bufferSize;
	}
	//#endregion

	//#region maskImageTop
	/**
	 * 设置阴影图片在背景图片中纵向位置 （从上往下 像素）
	 * @param top 阴影图片在背景图片中纵向位置
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setMaskImageTop(int top) {
		this.maskImageTop = top;
	}
	
	/**
	 * 获取阴影图片在背景图片中纵向位置 （从上往下 像素）
	 * @return 阴影图片在背景图片中纵向位置
	 * 2023年4月7日
	 * @author MBG
	 */
	public int getMaskImageTop() {
		return maskImageTop;
	}
	//#endregion
	
	//#region blockImageName
	/**
	 * 设置滑块图片文件名
	 * @param blockImageName 滑块图片文件名
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setBlockImageName(String blockImageName) {
		if(blockImageName==null || blockImageName.length()<1) {
			return;
		}
		this.blockImageName = blockImageName;
	}
	
	/**
	 * 获取滑块图片文件名
	 * @return 滑块图片文件名
	 * 2023年4月7日
	 * @author MBG
	 */
	public String getBlockImageName() {
		return blockImageName;
	}
	//#endregion

	//#region maskImageName
	/**
	 * 设置阴影图片文件名
	 * @param maskImageName 阴影图片文件名
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setMaskImageName(String maskImageName) {
		if(maskImageName==null || maskImageName.length()<1) {
			return;
		}
		this.maskImageName = maskImageName;
	}
	
	/**
	 * 获取阴影图片文件名
	 * @return 阴影图片文件名
	 * 2023年4月7日
	 * @author MBG
	 */
	public String getMaskImageName() {
		return maskImageName;
	}
	//#endregion

	//#region bgImageFileCount
	/**
	 * 设置背景图片总数
	 * @param count 背景图片总数
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setBgImageFileCount(int count) {
		this.bgImageFileCount = count;
	}
	
	/**
	 * 获取背景图片总数
	 * @return 背景图片总数
	 * 2023年4月7日
	 * @author MBG
	 */
	public int getbgImageFileCount() {
		return bgImageFileCount;
	}
	//#endregion

	//#region bgImageFileNameHead
	/**
	 * 设置背景图片文件名前缀（默认为 bg_）
	 * @param fileNameHead 背景图片文件名前缀
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setBgImageFileNameHead(String fileNameHead) {
		if(fileNameHead==null) {
			fileNameHead = "";
		}
		this.bgImageFileNameHead = fileNameHead;
	}
	
	/**
	 * 获取背景图片文件名前缀
	 * @return 背景图片文件名前缀
	 * 2023年4月7日
	 * @author MBG
	 */
	public String getBgImageFileNameHead() {
		return bgImageFileNameHead;
	}
	//#endregion

	//#region imageBasePath
	/**
	 * 设置图片根路径（绝对路径）
	 * @param basePath 图片根路径
	 * 2023年4月7日
	 * @author MBG
	 */
	public void setImageBasePath(String basePath) {
		this.imageBasePath = basePath;
	}

	/**
	 * 返回图片根路径
	 * @return 图片根路径
	 * 2023年4月7日
	 * @author MBG
	 */
	public String getImageBasePath() {
		return imageBasePath;
	}
	//#endregion
	
	//#endregion

	//#region 【对外方法区】
	
	//#region check(req,value) 判断提交请求上来的图快横向位置值与会话中的值是否匹配
	/**
	 * 判断提交请求上来的图快横向位置值与会话中的值是否匹配
	 * @param req   页面请求对象
	 * @param value 页面请求提交图块横向位置值
	 * @return      true匹配
	 * 2023年4月7日
	 * @author MBG
	 */
	public boolean check(IRequest req,String value) {
		//#region 从会话中获取当前随机横向位置值
		int sVal = sint(req.iGetSession().getAttribute(valueSessionKey));
		if(sVal<1) {
			return false;
		}
		req.iGetSession().removeAttribute(valueSessionKey);
		//#endregion
		
		//#region 处理提交值
		int val = sint(value);
		if(val<1) {
			return false;
		}
		//#endregion
		
		//#region 执行比对判断
		if(val>sVal) {
			if(Math.abs(val-sVal)<errorsValue) {
				return true;
			}
			return false;
		}
		if(val<sVal) {
			if(Math.abs(sVal-val)<errorsValue) {
				return true;
			}
			return false;
		}
		// val == sVal
		return true;
		//#endregion
	}
	//#endregion
	
	//#region check(req) 判断提交请求上来的图快横向位置值与会话中的值是否匹配
	/**
	 * 判断提交请求上来的图快横向位置值与会话中的值是否匹配
	 * @param req 页面请求对象
	 * @return    true匹配
	 * 2023年4月7日
	 * @author MBG
	 */
	public boolean check(IRequest req) {
		return check(req,req.getParameter(valueRequestKey));
	}
	//#endregion
	
	//#region checkSet(req,value) 验证图块位置成功后，将随机数设置到指定会话主键中，并返回随机数值
	/**
	 * 验证图块位置成功后，将随机数设置到指定会话主键中，并返回随机数值（腿甲模式2）
	 * @param req    请求对象
	 * @param value  图块横向位置值
	 * @return       设置到指定会话主键中的随机数值
	 * 2023年4月7日
	 * @author MBG
	 */
	public String checkSet(IRequest req,String value) {
		if(otherCheckSessionKey.length()<1 || !check(req,value)) {
			return "";
		}
		// 构建返回随机数值
		String reValue = String.valueOf(System.currentTimeMillis())+"_"+getIndex();
		// 设置到会话中
		req.iGetSession().setAttribute(otherCheckSessionKey,reValue);
		return reValue;
	}
	//#endregion
	
	//#region checkSet(req) 验证图块位置成功后，将随机数设置到指定会话主键中，并返回随机数值
	/**
	 * 验证图块位置成功后，将随机数设置到指定会话主键中，并返回随机数值（腿甲模式2）
	 * @param req  请求对象
	 * @return     设置到指定会话主键中的随机数值
	 * 2023年4月7日
	 * @author MBG
	 */
	public String checkSet(IRequest req) {
		return checkSet(req,req.getParameter(valueRequestKey));
	}
	//#endregion
	
	//#region outBlockImage(resp) 输出滑块图片到前台 
	/**
	 * 输出滑块图片到前台
	 * @param resp       输出对象
	 * @throws Exception 异常
	 * 2023年4月7日
	 * @author MBG
	 */
	public void outBlockImage(IResponse resp) throws Exception {
		//构建下载文件对象
        File downLoadFile = new File(getImageBasePath()+"/"+getBlockImageName());
        if(!downLoadFile.exists()) {
        	throw new Exception(
        			"Not Find The Block Image File["+getBlockImageName()
        			+"] In Path:["+getImageBasePath()+"]");
        }
		resp.flushBuffer();
		resp.setContentType("image/png");
		resp.setHeader("Cache-Control", "no-cache");
		resp.setHeader("Pragma", "no-cache");
		resp.setDateHeader("Expires", 0); 
		
        //构建文件读取流
        FileInputStream fis = null;
		try {
	        fis = new FileInputStream(downLoadFile);
	        //缓存数组
	        byte[] bufferBytes = new byte[getBufferSize()];
	        //获取文件大小
	        int fileSize = (int)downLoadFile.length();
	        //需要转换成ISO-8859-1 避免汉字乱码
	        resp.setHeader(
	        		"Content-Disposition"
	        		,"inline;filename=\""+getBlockImageName());
	        resp.setContentLength(fileSize);
	        int readCount = 0; //读取字节数
	        while(readCount<fileSize){
	        	//获取本次读取字节数
	            int readByteCount = fis.read(bufferBytes,0,getBufferSize());
	            readCount += readByteCount; //累加读取字节
	            //输出页面
	            resp.iGetOutputStream().write(bufferBytes,0,readByteCount);
	        }
        }catch(Exception e) {
    		//忽略客户端放弃下载异常
    		return;
        }finally {
            try {
                fis.close();
            }catch(Exception e) {}
        }
	}
	//#endregion
	
	//#region outImage(req,resp) 输出带阴影的背景图片到页面
	/**
	 * 输出带阴影的背景图片到页面
	 * @param req        页面请求对象
	 * @param resp       反馈对象
	 * @return           阴影在背景图片中的位置（从左到右 像素）
	 * @throws Exception 异常
	 * 2023年4月7日
	 * @author MBG
	 */
	public String outImage(IRequest req,IResponse resp) throws Exception {
		//设置允许输出图片
		System.setProperty("java.awt.headless","true");
		// 随机获取背景图片对象
		BufferedImage bgBi = getBgImage();
		// 获取阴影对象
		BufferedImage mBi  = getMaskImage();
		//#region 获取阴影横向位置值
		int randX = bgBi.getWidth()/10*3;
		randX += random.nextInt(bgBi.getWidth()/10*6);
		// 构建返回值
		String reStr = String.valueOf(randX);
		// 设置到会话中
		req.iGetSession().setAttribute(valueSessionKey,reStr);
		//#endregion
		
		//#region 开始画图
		
		//#region 构建相同背景大小的画板
		Graphics2D bg2d = bgBi.createGraphics();
        BufferedImage imageNew = 
        		bg2d.getDeviceConfiguration().createCompatibleImage(
        				bgBi.getWidth(), bgBi.getHeight(),Transparency.TRANSLUCENT);
        bg2d.dispose();
        bg2d = imageNew.createGraphics();
        //#endregion
        
        // 画背景
        bg2d.drawImage(bgBi, 0, 0, bgBi.getWidth(), bgBi.getHeight(), null);

        //#region 画阴影
        bg2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
        bg2d.drawImage(mBi, randX, maskImageTop>0?maskImageTop:bgBi.getHeight()/4, mBi.getWidth(), mBi.getHeight(), null);
        bg2d.dispose();
        //#endregion
        
        //#region 输出到页面
		resp.flushBuffer();
		resp.setContentType("image/png");
		resp.setHeader("Cache-Control", "no-cache");
		resp.setHeader("Pragma", "no-cache");
		resp.setDateHeader("Expires", 0); 
		// 输出图象到页面内存中计算图片大小
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ImageIO.write(imageNew,"png",bos);
		// 设置长度
		resp.setContentLength(bos.size());
		// 获取输出流
		OutputStream os = null;
		try {
			os = resp.iGetOutputStream();
			os.write(bos.toByteArray());
			os.flush();
		}catch(Exception e) {}
		finally {
			try {
				os.close();
			}catch(Exception e) {}
		}
        //#endregion
        
        //#endregion
        
		return reStr;
	}
	//#endregion
	
	//#endregion
	
	//#region 【对内方法区】
	
	//#region sint 将对象转换为数字
	/**
	 * 将对象转换为数字
	 * @param val 数字对象
	 * @return    转换后的数字
	 * 2023年4月7日
	 * @author MBG
	 */
	private int sint(Object val) {
		if(val==null) {
			return 0;
		}
		if(val instanceof Integer) {
			return (Integer)val;
		}
		// 转换为字符串值
		String valString = val.toString();
		int    point = valString.indexOf(".");
		if(point>-1) {
			valString = valString.substring(0,point);
		}
		if(valString.length()<1) {
			return 0;
		}
		try {
			return Integer.parseInt(valString);
		}catch(Exception e) {}
		return 0;
	}
	//#endregion
	
	//#region getIndex() 返回索引值
	/**
	 * 返回索引值
	 * @return 索引值
	 * 2023年4月7日
	 * @author MBG
	 */
	public long getIndex() {
		if(index>=Long.MAX_VALUE) {
			index = 0;
		}
		return index++;
	}
	//#endregion
	
	//#region getBgImage() 随机获取背景图片文件对象
	/**
	 * 随机获取背景图片对象
	 * @return           背景图片对象
	 * @throws Exception 异常
	 * 2023年4月7日
	 * @author MBG
	 */
	private BufferedImage getBgImage() throws Exception {
		if(bgImageFileCount<1) {
			throw new Exception("THe bgImageFileCount vale Not set");
		}
		// 文件号随机数（从1开始，包含bgImageFileCount值）
		int imageNo = random.nextInt(bgImageFileCount)+1;
		// 背景图片文件名
		String fileName = bgImageFileNameHead+imageNo+".png";
		// 获取对应的文件对象
		File imgFile = new File(imageBasePath+"/"+fileName);
		if(!imgFile.exists()) {
			throw new Exception("Not found the bgImage File:["+fileName+"] In Path:["+imageBasePath+"]");
		}
		return ImageIO.read(imgFile);
	}
	//#endregion
	
	//#region getMaskImage() 获取阴影图片缓存对象
	/**
	 * 获取阴影图片缓存对象
	 * @return             阴影图片缓存对象
	 * @throws Exception   异常
	 * 2023年4月7日
	 * @author MBG
	 */
	private BufferedImage getMaskImage() throws Exception {
		if(maskBi==null) {
			// 获取阴影图片文件对象
			File mFile = new File(imageBasePath+"/"+maskImageName);
			if(!mFile.exists()) {
				throw new Exception("Not Found The Mask Image File:["
						+maskImageName+"] In Path:["+imageBasePath+"]");
			}
			maskBi  = ImageIO.read(mFile);
		}
		return maskBi;
	}
	//#endregion
	 
	//#endregion
}







